package com.advision.android;

import java.util.*;


import android.content.*;
import android.graphics.*;
import android.graphics.Paint.*;
import android.graphics.drawable.*;
import android.util.*;
import android.view.*;

/**
 * The actual view containing an advertisment.  It is always embedded within an <code>AdView</code>
 * (which is responsible for animations).  This class contains all of the ad rendering logic.
 * 
 * @see AdView
 */
class AdContainer
	extends View
{
	/**
	 * The maximum number of pixels wide the interior of an ad can be.  Screens wider than this will
	 * have the background stretch across them with the ad horizontally centered and drawn to this size.
	 */
	public static final int MAX_WIDTH = 320;

	/**
	 * The height of all ads.
	 */
	public static final int HEIGHT = 48;

	/**
	 * The starting color for the ad background.  A gradient is applied over this to
	 * give it a shine.
	 */
	private static final int GRADIENT_BACKGROUND_COLOR = Color.WHITE;

	/**
	 * Unless <code>setBackgroundColor</code> is called the ad's background, what is behind the
	 * gradient, will be this color.
	 */
	public static final int DEFAULT_BACKGROUND_COLOR = Color.BLACK;
	
	/**
	 * The alpha value at the very top of the gradient creating the shine for the ad.
	 * The alpha gradually becomes opaque once it gets <code>GRADIENT_STOP</code> of
	 * the way down the ad.  This is 50% for the iPhone.
	 */
	private static final int GRADIENT_TOP_ALPHA = (int)( 255 * 0.50 );  // 50% opaque
	
	/**
	 * The percentage of the view's height from the top at which the gradient background
	 * becomes fully opaque.  This makes what is above this point look like a light is
	 * shinging from above and reflecting.  What is below this point is in the shadow.
	 * This is 0.4375 for the iPhone.
	 */
	private static final double GRADIENT_STOP = 0.4375;

	/**
	 * The number of pixels surrounding the icon image and text on all sides.
	 */
	private static final int PADDING = 8;

	/**
	 * The number of pixels between two lines of text such as the ad's text and "Ads by AdMob".
	 */
	private static final int TEXT_SPACING = 4;
	
	/**
	 * Unless <code>setTextColor</code> is called the ad's text will be this color.
	 */
	public static final int DEFAULT_TEXT_COLOR = Color.WHITE;

	/**
	 * The font used to render the ad's text.  iPhone ads use Helvetica-Bold which is in the
	 * Sans Serif family.
	 */
	private static final Typeface AD_FONT = Typeface.create( Typeface.SANS_SERIF, Typeface.BOLD );
	
	/**
	 * The font size used to render the ad's text.  iPhone ads uses 13 but the actual height is 16 pixels
	 * because of letters like 'y' which dip below the font.
	 */
	private static final float AD_FONT_SIZE = 13.0f;
	
	/**
	 * The "Ads by AdMob" text that appears below text ads that are one line long.
	 */
	private static final String ADS_BY_ADMOB = "Ads by AdMob";
	
	/**
	 * The font used to render "Ads by AdMob".  iPhone ads use Helvetica.
	 */
	private static final Typeface ADS_BY_ADMOB_FONT = Typeface.create( Typeface.SANS_SERIF, Typeface.NORMAL );
	
	/**
	 * The font size used to render the ad's text.  iPhone ads uses 9.5 pixels.
	 */
	private static final float ADS_BY_ADMOB_FONT_SIZE = 9.5f;

	/**
	 * The color of a mask applied over the entire view when the user presses down on
	 * the ad to signal they are going to click it.  iPhone uses a 0.5 alpha blended with
	 * grey 1/2 way on the greyscale (0x88888888).
	 */
	private static final int HIGHLIGHT_COLOR = 0xFFFFB400;  // Android yellow

	/**
	 * The color applied to a highlighted ad before the "shine" of <code>HIGHLIGHT_COLOR</code>
	 * is applied.  This color is analygous to <code>GRADIENT_BACKGROUND_COLOR</code>.
	 */
	private static final int HIGHLIGHT_BACKGROUND_COLOR = 0xFFEE7F27;  // Android orange

	/**
	 * The color of the ad text when it is being clicked.  This will be writter over the
	 * <code>HIGHLIGHT_COLOR</code>.
	 */
	private static final int HIGHLIGHT_TEXT_COLOR = Color.BLACK;

	/**
	 * Color of the ring around the ad when it has the focus.
	 */
	private static final int FOCUS_COLOR = HIGHLIGHT_BACKGROUND_COLOR;
	
	/**
	 * Width of the ring around the ad when it has the focus.
	 */
	private static final float FOCUS_WIDTH = 2.0f;
	
	/**
	 * Radius of the rounded corners to the ring around the ad when it has the focus.
	 */
	private static final float FOCUS_CORNER_ROUNDING = 3.0f;
	
	/**
	 * The color used in the background shine of the ad.  The user can set this to make
	 * the ad appear greener, for example.
	 */
	private int backgroundColor = DEFAULT_BACKGROUND_COLOR;

	/**
	 * The color of the ad's text and of "Ads by AdMob".
	 */
	private int textColor = DEFAULT_TEXT_COLOR;
	
	/**
	 * The object used to write the ad's text.
	 */
	private Paint textPaint;

	/**
	 * The object used to write "Ads by AdMob".
	 */
	private Paint adsByAdmobPaint;
	
	/**
	 * The ad displayed by this view.  If this is <code>null</code> this view will not be visible.
	 */
	private final Ad ad;

	/**
	 * Constructs an advertisement view manually (not from a layout XML file).
	 * 
	 * @param ad is the object containing an ad fetched from AdMob servers.
	 * @param context is the application's context.
	 * 
	 * @see android.view.View#View(Context)
	 */
	public AdContainer( Ad ad, Context context )
	{
		this( ad, context, null, 0 );
	}

	/**
	 * Constructs an advertisment view from a layout XML file.
	 * 
	 * @param ad is the object containing an ad fetched from AdMob servers.
	 * @param context is the application's context.
	 * @param attrs are attributes set in the XML layout for this view.
	 * 
	 * @see android.view.View#View(android.content.Context, android.util.AttributeSet)
	 */
	public AdContainer( Ad ad, Context context, AttributeSet attrs )
	{
		this( ad, context, attrs, 0 );
	}

	/**
	 * Constructs an advertisment view from a layout XML file.
	 * 
	 * @param ad is the object containing an ad fetched from AdMob servers.
	 * @param context is the application's context.
	 * @param attrs are attributes set in the XML layout for this view.
	 * @param defStyle is the theme ID to apply to this view.
	 * 
	 * @see android.view.View#View(Context, AttributeSet, int)
	 */
	public AdContainer( Ad ad, Context context, AttributeSet attrs, int defStyle )
	{
		super( context, attrs, defStyle );
		
		this.ad = ad;
		
		// The user can interact with the ad by clicking on it.
		if ( ad != null )
		{
			setFocusable( true );
			setClickable( true );
		}
		
		// Create the paint objects used to render parts of the ad.
		textPaint = new Paint();
		textPaint.setAntiAlias( true );
		textPaint.setTypeface( AD_FONT );
		textPaint.setTextSize( AD_FONT_SIZE );

		adsByAdmobPaint = new Paint();
		adsByAdmobPaint.setAntiAlias( true );
		adsByAdmobPaint.setTypeface( ADS_BY_ADMOB_FONT );
		adsByAdmobPaint.setTextSize( ADS_BY_ADMOB_FONT_SIZE );

		// Apply any custom attributes from the XML layout.
		int tc = DEFAULT_TEXT_COLOR;
		int bc = DEFAULT_BACKGROUND_COLOR;
		
		if ( attrs != null )
		{
			String namespace = "http://schemas.android.com/apk/res/" + context.getPackageName();
			
			tc = attrs.getAttributeUnsignedIntValue( namespace, "textColor", DEFAULT_TEXT_COLOR );
			bc = attrs.getAttributeUnsignedIntValue( namespace, "backgroundColor", DEFAULT_BACKGROUND_COLOR );
		}
		
		setTextColor( tc );
		setBackgroundColor( bc );
	}

	/**
	 * Sets the ad's text color.
	 * 
	 * @param color ARGB value for the ad text.
	 * 
	 * @see android.R.color
	 */
	public void setTextColor( int color )
	{
		this.textColor = 0xFF000000 | color;  // Remove the alpha channel
		postInvalidate();
	}
	
	/**
	 * @return The text ARGB color value for the ad text.
	 * 
	 * @see android.R.color
	 */
	public int getTextColor()
	{
		return textColor;
	}
	
	/**
	 * Sets the ad's background color.  A "shine" is applied over it which appears as
	 * to reflect light from above the ad.
	 * 
	 * @param color ARGB value for the ad.
	 * 
	 * @see android.R.color
	 */
	public void setBackgroundColor( int color )
	{
		this.backgroundColor = 0xFF000000 | color;  // Remove the alpha channel
		postInvalidate();
	}
	
	/**
	 * @return The background ARGB color value for the ad.
	 * 
	 * @see android.R.color
	 */
	public int getBackgroundColor()
	{
		return backgroundColor;
	}
	
	/**
	 * @return The ad object shown by this view or <code>null</code> if this view is
	 *  empty.
	 */
	protected Ad getAd()
	{
		return ad;
	}
	
	/**
	 * Calculates the size of the ad view.
	 * 
	 * @see android.view.View#measure(int, int)
	 */
	@Override
	protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec )
	{
		int w = 0;
		int h = 0;
		
		h = measureHeight( heightMeasureSpec );
		
		if ( h != 0 )
		{
			w = measureWidth( widthMeasureSpec );
		}
		
		setMeasuredDimension( w, h );
		
		if ( Log.isLoggable( AdManager.LOG, Log.DEBUG ) )
		{
			Log.d( AdManager.LOG, "AdContainer.onMeasure() determined the ad to be " + w + "x" + h + " pixels." );
		}
		
		// Change the font sizes to reflect the width.
		if ( w > 0 )
		{
			if ( w <= 128 )
			{
				textPaint.setTextSize( AD_FONT_SIZE * 0.70f );
				adsByAdmobPaint.setTextSize( ADS_BY_ADMOB_FONT_SIZE * 0 );  // Never show
			}
			else if ( w <= 176 )
			{
				textPaint.setTextSize( AD_FONT_SIZE * 0.8f );
				adsByAdmobPaint.setTextSize( ADS_BY_ADMOB_FONT_SIZE * 0.8f );
			}
			else
			{
				textPaint.setTextSize( AD_FONT_SIZE );
				adsByAdmobPaint.setTextSize( ADS_BY_ADMOB_FONT_SIZE );
			}
		}
	}

	/**
	 * Determines the width of this view.
	 * 
	 * @param measureSpec A measureSpec packed into an int
	 * @return The width of the view, honoring constraints from measureSpec
	 */
	private int measureWidth( int measureSpec )
	{
		// The ad should always be as wide as possible.
		int specSize = MeasureSpec.getSize( measureSpec );
		return specSize;
	}

	/**
	 * Determines the height of this view.
	 * 
	 * @param measureSpec A measureSpec packed into an int
	 * @return The height of the view, honoring constraints from measureSpec
	 */
	private int measureHeight( int measureSpec )
	{
		int result = 0;
		int specMode = MeasureSpec.getMode( measureSpec );
		int specSize = MeasureSpec.getSize( measureSpec );

		if ( specMode == MeasureSpec.EXACTLY )
		{
			// We were told how big to be
			result = specSize;
		}
		else
		{
			result = HEIGHT;

			// Respect AT_MOST value if that was what is called for by measureSpec
			if ( specMode == MeasureSpec.AT_MOST )
			{
				result = Math.min( result, specSize );
			}
		}
		
		if ( result != HEIGHT )
		{
			Log.w( AdManager.LOG, "Cannot render the ad into " + result + " vertical pixels because it must be " + HEIGHT + " tall." );
			result = 0;
		}
		
		return result;
	}
	
	/**
	 * Render the ad.
	 * 
	 * @see android.view.View#onDraw(android.graphics.Canvas)
	 */
	@Override
	protected void onDraw( Canvas canvas )
	{
		try
		{
			super.onDraw( canvas );
			
			if ( ad != null )
			{
				// Calculate the dimensions of the ad.
				int width = getWidth();
				Rect viewRect = new Rect( 0, 0, width, getHeight() );
				
				// Calculate the position of the icon and text.
				Bitmap icon = ad.getIcon();
				Rect iconRect = null;
				Rect adRect;
				
				if ( icon != null )
				{
					int iconWidth = icon.getWidth();
					int iconHeight = icon.getHeight();
					int iconIndent = ( viewRect.height() - iconHeight ) / 2;
					
					iconRect = new Rect( 
							viewRect.left + PADDING, 					// left
							viewRect.top + iconIndent,					// top
							viewRect.left + PADDING + iconWidth,		// right
							viewRect.top + iconIndent + iconHeight );	// bottom
					
					adRect = new Rect( 
							iconRect.right + PADDING, 
							viewRect.top, 
							viewRect.right - PADDING,
							viewRect.bottom );
				}
				else
				{
					adRect = new Rect( 
							viewRect.left + PADDING, 
							viewRect.top, 
							viewRect.right - PADDING,
							viewRect.bottom );
				}
				
				// Render the ad.
				if ( isPressed() )
				{
					// Draw a highlight over the ad to show it is being clicked.
					drawBackground( canvas, viewRect, HIGHLIGHT_BACKGROUND_COLOR, HIGHLIGHT_COLOR );
					
					// Draw the text.
					drawText( canvas, adRect, HIGHLIGHT_TEXT_COLOR );
				}
				else
				{
					// Draw a regular background.
					drawBackground( canvas, viewRect, GRADIENT_BACKGROUND_COLOR, backgroundColor );
					
					// Draw the text.
					drawText( canvas, adRect, textColor );
					
					// Draw a focus ring around the ad if it has focus.
					if ( hasFocus() )
					{
						drawFocusRing( canvas, viewRect );
					}
				}
				
				if ( icon != null )
				{
					// Draw the icon image last.
					drawIcon( canvas, iconRect, icon );
				}
			}
		}
		catch (Exception e)
		{
			Log.e( AdManager.LOG, "Exception raised during onDraw.", e );
		}
	}
	
	/**
	 * Draws the background for the ad.  Drawing this is the first step in drawing an ad.
	 * 
	 * @param c is the canvas the background is drawn onto.
	 * @param r is the dimensions to draw the background into.
	 * @param background color is the first color painted with the <code>color</code>
	 *  being laid over it in the gradient.  It is the color seen at the very top of the ad.
	 * @param color is the background color of the ad.  If this ad is being clicked it
	 *  should be the standard Android orange highlight color.
	 */
	private static void drawBackground( Canvas c, Rect r, int backgroundColor, int color )
	{
		// First draw an opaque background.
		Paint paint = new Paint();
		paint.setColor( backgroundColor );
		paint.setAntiAlias( true );
		c.drawRect( r, paint );
		
		// Draw a "shine" or reflection from above on the top half.  It is a gradient that
		// becomes fully opaque part way down the ad.
		int topColor = Color.argb( GRADIENT_TOP_ALPHA, Color.red( color ), Color.green( color ), Color.blue( color ) );
		int[] gradientColors = { topColor, color };
		GradientDrawable shine = new GradientDrawable( GradientDrawable.Orientation.TOP_BOTTOM, gradientColors );
		
		int stop = (int)( r.height() * GRADIENT_STOP ) + r.top;
		shine.setBounds( r.left, r.top, r.right, stop );
		shine.draw( c );
		
		// Draw the "shadow" below the reflection.  It is a fully opaque section below
		// where the gradient stops.
		Rect shadowRect = new Rect( r.left, stop, r.right, r.bottom );
		Paint shadowPaint = new Paint();
		shadowPaint.setColor( color );
		c.drawRect( shadowRect, shadowPaint );
	}

	/**
	 * Draws the ad's icon image to the left of the ad text.  The tile represents the
	 * general category of ad such as a "Download".  This gives the user a visual cue
	 * about what the ad is for.
	 * 
	 * @param c is the canvas the ad image and text are drawn onto.
	 * @param r is the dimensions the ad image and text are placed within.
	 * @param tile is the icon image.
	 */
	private static void drawIcon( Canvas c, Rect r, Bitmap tile )
	{
		Paint paint = new Paint();
		c.drawBitmap( tile, r.left, r.top, paint );
	}
	
	/**
	 * Draws the ad's text.  This is the last step in drawing an ad.
	 * <p>
	 * The text is vertically centered and left-justified.  If there is only one line
	 * of text than "Ads by AdMob" is also painted as a second line that is right-justified.
	 * 
	 * @param c is the canvas the ad image and text are drawn onto.
	 * @param r is the dimensions the ad image and text are placed within.
	 * @param textColor is the color of the ad's text.
	 */
	private void drawText( Canvas c, Rect r, int adTextColor )
	{
		// Size the ad's text.
		String[] lines = null;
		float textHeight = 0;
		float textLineHeight = 0;
		float adsByAdmobLineHeight = 0;
		
		String text = ad.getText();
		
		if ( text != null )
		{
			// Set the text color.
			textPaint.setColor( adTextColor );
			adsByAdmobPaint.setColor( adTextColor );
			
			// Break the ad's text so it fits within the boundaries.
			lines = breakIntoLines( textPaint, text, r.width() );

			// Get the total height of the ad's text.
			FontMetrics fontMetrics = textPaint.getFontMetrics();
			textLineHeight = fontMetrics.descent - fontMetrics.ascent;
			textHeight = textLineHeight * lines.length;
			
			// If the ad text fits on one line make a second "Ads by AdMob" line.
			if ( (lines.length == 1) && (ad.hasImage() == false) && (AdManager.showAdsByAdmob()) )
			{
				// Does "Ads by AdMob" fit onto one line?
				if ( breakIntoLines( adsByAdmobPaint, ADS_BY_ADMOB, r.width() ).length == 1 )
				{
					FontMetrics admobFontMetrics = adsByAdmobPaint.getFontMetrics();
					adsByAdmobLineHeight = admobFontMetrics.descent - admobFontMetrics.ascent;
				}
				else
				{
					if ( Log.isLoggable( AdManager.LOG, Log.DEBUG ) )
					{
						Log.d( AdManager.LOG, "Cannot write \"" + ADS_BY_ADMOB + "\" because it does not fit onto one line." );
					}
				}
			}
		}
		
		// Calculate the vertical centering.
		float totalHeight = textHeight;
		
		if ( adsByAdmobLineHeight > 0 )  // there is "Ads by AdMob"
		{
			// Only if the ad isn't too tall already.
			if ( totalHeight + TEXT_SPACING + adsByAdmobLineHeight <= r.height() )
			{
				// Place padding below the ad text and above "Ads by AdMob".
				totalHeight += TEXT_SPACING;
				
				// The height of "Ads by AdMob".
				totalHeight += adsByAdmobLineHeight;
			}
			else
			{
				if ( Log.isLoggable( AdManager.LOG, Log.DEBUG ) )
				{
					Log.d( AdManager.LOG, "Cannot write \"" + ADS_BY_ADMOB + "\" because it will make the ad too tall." );
				}
				
				adsByAdmobLineHeight = 0;
			}
		}
		
		float top = ( r.height() - totalHeight ) / 2;
		
		if ( top < 0 )
		{
			Log.e( AdManager.LOG, "The ad's calculated height (" + totalHeight + ") is bigger than the view height (" + r.height() + ")." );
			top = 0;
		}
		
		top += r.top;
		
		// Draw the ad's text.
		if ( lines != null )
		{
			int ascent = (int) textPaint.ascent();
			top += -ascent;
			
			for ( String line : lines )
			{
				c.drawText( line, r.left, top, textPaint );
				top += textLineHeight;
			}
			
			// Draw "Ads by AdMob".
			if ( adsByAdmobLineHeight > 0 )
			{
				top += ascent;
				ascent = (int) adsByAdmobPaint.ascent();
				
				float left = r.right - adsByAdmobPaint.measureText( ADS_BY_ADMOB );
				top += TEXT_SPACING + -ascent;
				c.drawText( ADS_BY_ADMOB, left, top, adsByAdmobPaint );
			}
		}
	}

	/**
	 * Draws a focus ring around the ad when it has been scrolled over.  This is the last step
	 * in rendering the ad.
	 * 
	 * @param c is the canvas the ring is drawn onto.
	 * @param r is the rectangle that specifies the path of the ring.
	 * @param color is the focus ring color around the ad.
	 */
	private static void drawFocusRing( Canvas c, Rect r )
	{
		Paint paint = new Paint();
		paint.setAntiAlias( true );
		paint.setColor( FOCUS_COLOR );
		paint.setStyle( Paint.Style.STROKE );
		paint.setStrokeWidth( FOCUS_WIDTH );
		paint.setPathEffect( new CornerPathEffect( FOCUS_CORNER_ROUNDING ) );

		Path path = new Path();
		path.addRoundRect( new RectF( r ), FOCUS_CORNER_ROUNDING, FOCUS_CORNER_ROUNDING, Path.Direction.CW );
		
		c.drawPath( path, paint );
	}
	
	/**
	 * Called when the user presses any key while the focus is on this ad.
	 *
	 * @see android.view.View#onKeyDown(int, android.view.KeyEvent)
	 */
	@Override
	public boolean onKeyDown( int keyCode, KeyEvent event )
	{
		if ( Log.isLoggable( AdManager.LOG, Log.VERBOSE ) )
		{
			Log.v( AdManager.LOG, "onKeyDown:" + " keyCode="+keyCode );
		}
		
		if ( keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER )
		{
			setPressed( true );
		}
		
		// Continue processing the event.
		return super.onKeyDown( keyCode, event );
	}

	/**
	 * Called when the user releases any key while the focus is on this ad.
	 *
	 * @see android.view.View#onKeyUp(int, android.view.KeyEvent)
	 */
	@Override
	public boolean onKeyUp( int keyCode, KeyEvent event )
	{
		if ( Log.isLoggable( AdManager.LOG, Log.VERBOSE ) )
		{
			Log.v( AdManager.LOG, "onKeyUp:" + " keyCode="+keyCode );
		}
		
		if ( keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER )
		{
			click();
		}
		
		setPressed( false );
		
		// Continue processing the event.
		return super.onKeyUp( keyCode, event );
	}

	/**
	 * Called when the user presses the touch screen for this ad.
	 *
	 * @see android.view.View#dispatchTouchEvent(android.view.MotionEvent)
	 */
	@Override
	public boolean dispatchTouchEvent( MotionEvent e )
	{
		// Note onTouchEvent() does not work like the expected so we need to hook into the dispatch.
		
		int action = e.getAction();

		if ( Log.isLoggable( AdManager.LOG, Log.VERBOSE ) )
		{
			Log.v( AdManager.LOG, "dispatchTouchEvent:" + " action="+action + " x="+e.getX() + " y="+e.getY() );
		}
		
		if ( action == MotionEvent.ACTION_DOWN )
		{
			setPressed( true );
		}
		else if ( action == MotionEvent.ACTION_MOVE )
		{
			// Determine if this event is within the view or has left the view.
			float x = e.getX();
			float y = e.getY();
			
			int left = getLeft();
			int top = getTop();
			int right = getRight();
			int bottom = getBottom();
	
			if ( x < left || x > right || y < top || y > bottom )
			{
				// No longer holding the view down.
				setPressed( false );
			}
			else
			{
				setPressed( true );
			}
		}
		else if ( action == MotionEvent.ACTION_UP )
		{
			if ( isPressed() )
			{
				click();
			}
			
			setPressed( false );
		}
		
		// Continue processing the event.
		return super.dispatchTouchEvent( e );
	}
	
	/**
	 * Called when the user presses the trackball while the focus is on this ad.
	 *
	 * @see android.view.View#dispatchTrackballEvent(android.view.MotionEvent)
	 */
	@Override
	public boolean dispatchTrackballEvent( MotionEvent event )
	{
		if ( Log.isLoggable( AdManager.LOG, Log.VERBOSE ) )
		{
			Log.v( AdManager.LOG, "dispatchTrackballEvent:" + " action="+event.getAction() );
		}
		
		if ( event.getAction() == MotionEvent.ACTION_DOWN )
		{
			setPressed( true );
		}
		else if ( event.getAction() == MotionEvent.ACTION_UP )
		{
			if ( hasFocus() )
			{
				click();
			}
			
			setPressed( false );
		}
		
		// Continue processing the event.
		return super.onTrackballEvent( event );
	}

	/**
	 * Call whenever this ad is getting pressed, but before it has been clicked.  For example
	 * when a key goes down, but before it goes up.
	 * 
	 * @param pressed when <code>true</code> renders this ad that a click will take place and
	 *  when <code>false</code> renders the ad normally.
	 */
	@Override
	public void setPressed( boolean pressed )
	{
		if ( isPressed() != pressed )
		{
			super.setPressed( pressed );
			invalidate();
		}
	}
	
	/**
	 * Call when the user clicks on the ad.  This will bring up the ad's landing page in the
	 * browser and minimize this application.
	 */
	private void click()
	{
		if ( ad != null )
		{
			if ( isPressed() )
			{
				ad.clicked();
				setPressed( false );
			}
		}
	}
	
	/**
	 * Breaks <code>string</code> apart at line breaks.  The returned array has
	 * strings that can be written using <code>paint</code> in fewer than <code>width</code>
	 * pixels.
	 * <p>
	 * The <code>string</code> will be split apart at non-alphanumeric characters.
	 * However, if a string is too long and cannot be broken nicely, it will
	 * be split at the last alphanumeric character that fits the line
	 * <code>width</code>.
	 * <p>
	 * Newline characters (<code>'\n'</code>) are automatically made into line
	 * breaks.  An additional empty line, represented by a <code>null</code>
	 * string in the returned array, is added.  It can then be painted as
	 * a space to indicate a new paragraph.
	 * 
	 * @param paint is the <code>Paint</code> used to write <code>string</code>.
	 * @param string is the text to split.  If <code>null</code> then <code>null</code>
	 *  will be returned.
	 * @param width is the maximum number of pixels wide each string part
	 *  can be.
	 * @return An array of parts of <code>string</code> broken apart at line
	 *  breaks.
	 */
	private static String[] breakIntoLines( Paint paint, String string, float width )
	{
		String[] lines = null;
		
		if ( string != null )
		{
			// A container for holding lines as we parse them.
			LinkedList<String> parsedLines = new LinkedList<String>();
			String newLine;
			
			// Flag indicating if the current line starts a paragraph.
			// Spaces at the beginning of a paragraph are kept for
			// indentation or labels used as spacers.
			boolean startsParagraph = true;
			
			// The index of the character that starts this line.
			int lineStart = 0;
			
			// The index of a character that may appear on the
			// next line.  For example a space can appear on this
			// line or the next, but a letter in the middle of the
			// word must stay on the same line as the entire word.
			int lastBreakableSpot = 0;
			
			// The index of the last character that wasn't a
			// whitespace character such as ' '.
			int lastNonWhiteSpace = 0;
			
			// The width of a wide character used as a safety
			// margin in calculating the line width.  Not all font
			// width calculations are reliable (e.g. the default
			// font on the Motorola SLVR).  Adjusting the line
			// width by this amount assures no text will get clipped.
			float charWidth = paint.measureText( "0" );
			width -= charWidth;
			
			// Scan lengths character-by-character.
			char[] chars = string.toCharArray();
			
			for ( int i = 0; i < chars.length; i++ )
			{
				boolean isSeparator =
					(chars[i] == '-') ||
					(chars[i] == '/');
				boolean isWhiteSpace =
					(chars[i] == ' ');
				
				boolean isNewLine =
					(chars[i] == '\n');
				boolean isCarrageReturn =
					(chars[i] == '\r');
				
				boolean isLineBreak = isNewLine || isCarrageReturn;
				float lineWidth = paint.measureText( chars, lineStart, i - lineStart );

				// Are we done with this line?
				if ( (isLineBreak) || (lineWidth > width) )
				{
					int lineEnd;
					
					if ( isLineBreak )
					{
						lineEnd = i;
					}
					else if ( lastBreakableSpot > lineStart )
					{
						lineEnd = lastBreakableSpot;
					}
					else
					{
						// This word is longer than the line so do a mid-word break
						// with the current letter on the next line.
						lineEnd = i - 1;
					}
					
					// Record the line.
					newLine = string.substring( lineStart, lineEnd );
					newLine = smartTrim( newLine, startsParagraph );
					parsedLines.add( newLine );
					
					// Setup for the next line.
					if ( isLineBreak )
					{
						lineStart = lineEnd + 1;  // +1 to advance over the '\n'
						
						// Add an empty line between the paragraphs.
						if ( isNewLine )
						{
							parsedLines.add( null );
						}
						
						startsParagraph = true;
					}
					else  // line break in the middle of a paragraph
					{
						lineStart = lineEnd;
						startsParagraph = false;
					}
				}

				// Is this a good place for a line-break?
				if ( isSeparator )
				{
					// Put the separator char on this line (e.g. "full-").
					lastBreakableSpot = i + 1;
				}

				if ( isWhiteSpace )
				{
					// Break at the whitespace.  It will be trimmed later.
					lastBreakableSpot = lastNonWhiteSpace + 1;
				}
				else
				{
					// Record this character as the last non-whitespace processed.
					lastNonWhiteSpace = i;
				}
			}
			
			// Add the last line.
			newLine = string.substring( lineStart );
			newLine = smartTrim( newLine, startsParagraph );
			parsedLines.add( newLine );
			
			// Convert the vector into a string array.
			lines = new String[parsedLines.size()];
			parsedLines.toArray( lines );
		}
		
		return lines;
	}
	
	/**
	 * Trims whitespace from a string.  However, it does not trim leading
	 * whitespace if the string starts a paragraph.  That leading whitespace
	 * may be an indentation or used for spacing.
	 * 
	 * @param string is the string to trim.
	 * @param startsParagraph is <code>true</code> when <code>string</code>
	 *  is the start of a paragraph and <code>false</code> when it is not.
	 * @return A smartly trimmed version of <code>string</code>.
	 */
	private static String smartTrim( String string, boolean startsParagraph )
	{
		String trimmed;
		
		if ( startsParagraph )
		{
			// Trim the right side of the string only.
			int len = string.length() - 1;

			while ( (len >= 0) && (string.charAt(len) <= ' ') )
			{
			    len--;
			}
			
			if ( len <= 0 )
			{
				// All spaces.
				trimmed = string;
			}
			else
			{
				trimmed = string.substring( 0, len + 1 );
			}
		}
		else
		{
			trimmed = string.trim();
		}
		
		return trimmed;
	}
}
